// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global

/* PdfAcroField.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections.Generic;
using System.Diagnostics;

using AM;

using PdfSharpCore.Pdf.Advanced;

#endregion

#nullable enable

namespace PdfSharpCore.Pdf.AcroForms;

/// <summary>
/// Represents the base class for all interactive field dictionaries.
/// </summary>
public abstract class PdfAcroField
    : PdfDictionary
{
    #region Construction

    /// <summary>
    /// Initializes a new instance of PdfAcroField.
    /// </summary>
    internal PdfAcroField (PdfDocument document)
        : base (document)
    {
        // пустое тело конструктора
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PdfAcroField"/> class. Used for type transformation.
    /// </summary>
    protected PdfAcroField (PdfDictionary dict)
        : base (dict)
    {
        // пустое тело конструктора
    }

    #endregion

    /// <summary>
    /// Gets the name of this field.
    /// </summary>
    public string Name
    {
        get
        {
            var name = Elements.GetString (Keys.T);
            return name;
        }
    }

    /// <summary>
    /// Gets the field flags of this instance.
    /// </summary>
    public PdfAcroFieldFlags Flags =>

        // TODO: This entry is inheritable, thus the implementation is incorrect...
        (PdfAcroFieldFlags)Elements.GetInteger (Keys.Ff);

    internal PdfAcroFieldFlags SetFlags
    {
        get => (PdfAcroFieldFlags)Elements.GetInteger (Keys.Ff);
        set => Elements.SetInteger (Keys.Ff, (int)value);
    }

    /// <summary>
    /// Gets or sets the value of the field.
    /// </summary>
    public virtual PdfItem Value
    {
        get => Elements[Keys.V];
        set
        {
            if (ReadOnly)
            {
                throw new InvalidOperationException ("The field is read only.");
            }

            if (value is PdfString || value is PdfName)
            {
                Elements[Keys.V] = value;
            }
            else
            {
                throw new NotImplementedException ("Values other than string cannot be set.");
            }
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether the field is read only.
    /// </summary>
    public bool ReadOnly
    {
        get => (Flags & PdfAcroFieldFlags.ReadOnly) != 0;
        set
        {
            if (value)
            {
                SetFlags |= PdfAcroFieldFlags.ReadOnly;
            }
            else
            {
                SetFlags &= ~PdfAcroFieldFlags.ReadOnly;
            }
        }
    }

    /// <summary>
    /// Gets the field with the specified name.
    /// </summary>
    public PdfAcroField? this [string name] => GetValue (name);

    /// <summary>
    /// Gets a child field by name.
    /// </summary>
    protected virtual PdfAcroField? GetValue (string name)
    {
        if (string.IsNullOrEmpty (name))
        {
            return this;
        }

        if (HasKids)
        {
            return Fields.GetValue (name);
        }

        return null;
    }

    /// <summary>
    /// Indicates whether the field has child fields.
    /// </summary>
    public bool HasKids
    {
        get
        {
            var item = Elements[Keys.Kids];
            if (item == null)
            {
                return false;
            }

            if (item is PdfArray)
            {
                return ((PdfArray)item).Elements.Count > 0;
            }

            return false;
        }
    }

    /// <summary>
    /// Gets the names of all descendants of this field.
    /// </summary>
    public string[] GetDescendantNames()
    {
        var names = new List<string>();
        if (HasKids)
        {
            var fields = Fields;
            fields.GetDescendantNames (ref names, null);
        }

        var temp = new List<string>();
        foreach (var name in names)
        {
            temp.Add (name);
        }

        return temp.ToArray();
    }

    /// <summary>
    /// Gets the names of all appearance dictionaries of this AcroField.
    /// </summary>
    public string[] GetAppearanceNames()
    {
        var names = new Dictionary<string, object>();
        var dict = Elements["/AP"] as PdfDictionary;
        if (dict != null)
        {
            AppDict (dict, names);

            if (HasKids)
            {
                var kids = Fields.Elements.Items;
                foreach (var pdfItem in kids)
                {
                    if (pdfItem is PdfReference)
                    {
                        var xxx = ((PdfReference)pdfItem).Value as PdfDictionary;
                        if (xxx != null)
                        {
                            AppDict (xxx, names);
                        }
                    }
                }

                //((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1);
            }
        }

        var array = new string[names.Count];
        names.Keys.CopyTo (array, 0);
        return array;
    }

    //static string[] AppearanceNames(PdfDictionary dictIn)
    //{
    //  Dictionary<string, object> names = new Dictionary<string, object>();
    //  PdfDictionary dict = dictIn["/AP"] as PdfDictionary;
    //  if (dict != null)
    //  {
    //    AppDict(dict, names);

    //    if (HasKids)
    //    {
    //      PdfItem[] kids = Fields.Elements.Items;
    //      foreach (PdfItem pdfItem in kids)
    //      {
    //        if (pdfItem is PdfReference)
    //        {
    //          PdfDictionary xxx = ((PdfReference)pdfItem).Value as PdfDictionary;
    //          if (xxx != null)
    //            AppDict(xxx, names);
    //        }
    //      }
    //      //((PdfDictionary)(((PdfReference)(Fields.Elements.Items[1])).Value)).Elements.SetName(Keys.V, name1);

    //    }
    //  }
    //  string[] array = new string[names.Count];
    //  names.Keys.CopyTo(array, 0);
    //  return array;
    //}

    static void AppDict (PdfDictionary dict, Dictionary<string, object> names)
    {
        PdfDictionary sub;
        if ((sub = dict.Elements["/D"] as PdfDictionary) != null)
        {
            AppDict2 (sub, names);
        }

        if ((sub = dict.Elements["/N"] as PdfDictionary) != null)
        {
            AppDict2 (sub, names);
        }
    }

    static void AppDict2
        (
            PdfDictionary dict,
            Dictionary<string, object> names
        )
    {
        foreach (var key in dict.Elements.Keys)
        {
            if (!names.ContainsKey (key))
            {
                names.Add (key, null!);
            }
        }
    }

    internal virtual void GetDescendantNames
        (
            ref List<string> names,
            string? partialName
        )
    {
        if (HasKids)
        {
            var fields = Fields;
            var t = Elements.GetString (Keys.T);
            Debug.Assert (t != "");
            if (t.Length > 0)
            {
                if (!string.IsNullOrEmpty (partialName))
                {
                    partialName += "." + t;
                }
                else
                {
                    partialName = t;
                }

                fields.GetDescendantNames (ref names, partialName);
            }
        }
        else
        {
            var t = Elements.GetString (Keys.T);
            Debug.Assert (t != "");
            if (t.Length > 0)
            {
                if (!string.IsNullOrEmpty (partialName))
                {
                    names.Add (partialName + "." + t);
                }
                else
                {
                    names.Add (t);
                }
            }
        }
    }

    /// <summary>
    /// Gets the collection of fields within this field.
    /// </summary>
    public PdfAcroFieldCollection Fields
    {
        get
        {
            if (_fields == null)
            {
                var o = Elements.GetValue (Keys.Kids, VCF.CreateIndirect);
                _fields = (PdfAcroFieldCollection) o.ThrowIfNull ();
            }

            return _fields;
        }
    }

    PdfAcroFieldCollection? _fields;

    /// <summary>
    /// Holds a collection of interactive fields.
    /// </summary>
    public sealed class PdfAcroFieldCollection
        : PdfArray
    {
        PdfAcroFieldCollection (PdfArray array)
            : base (array)
        {
        }

        /// <summary>
        /// Gets the names of all fields in the collection.
        /// </summary>
        public string[] Names
        {
            get
            {
                var count = Elements.Count;
                var names = new string[count];
                for (var idx = 0; idx < count; idx++)
                {
                    names[idx] = ((PdfDictionary)((PdfReference)Elements[idx]).Value).Elements.GetString (Keys.T);
                }

                return names;
            }
        }

        /// <summary>
        /// Gets an array of all descendant names.
        /// </summary>
        public string[] DescendantNames
        {
            get
            {
                var names = new List<string>();
                GetDescendantNames (ref names, null);

                //List<string> temp = new List<string>();
                //foreach (PdfName name in names)
                //  temp.Add(name.ToString());
                return names.ToArray();
            }
        }

        internal void GetDescendantNames
            (
                ref List<string> names,
                string? partialName
            )
        {
            var count = Elements.Count;
            for (var idx = 0; idx < count; idx++)
            {
                var field = this[idx];
                if (field != null)
                {
                    field.GetDescendantNames (ref names, partialName);
                }
            }
        }

        /// <summary>
        /// Gets a field from the collection. For your convenience an instance of a derived class like
        /// PdfTextField or PdfCheckBox is returned if PDFsharp can guess the actual type of the dictionary.
        /// If the actual type cannot be guessed by PDFsharp the function returns an instance
        /// of PdfGenericField.
        /// </summary>
        public PdfAcroField this [int index]
        {
            get
            {
                var item = Elements[index];
                Debug.Assert (item is PdfReference);
                var dict = ((PdfReference)item).Value as PdfDictionary;
                Debug.Assert (dict != null);
                var field = dict as PdfAcroField;
                if (field == null && dict != null)
                {
                    // Do type transformation
                    field = CreateAcroField (dict);

                    //Elements[index] = field.XRef;
                }

                return field;
            }
        }

        /// <summary>
        /// Gets the field with the specified name.
        /// </summary>
        public PdfAcroField? this [string name] => GetValue (name);

        internal PdfAcroField? GetValue (string name)
        {
            if (string.IsNullOrEmpty (name))
            {
                return null;
            }

            var dot = name.IndexOf ('.');
            var prefix = dot == -1 ? name : name.Substring (0, dot);
            var suffix = dot == -1 ? "" : name.Substring (dot + 1);

            var count = Elements.Count;
            for (var idx = 0; idx < count; idx++)
            {
                var field = this[idx];
                if (field.Name == prefix)
                {
                    return field.GetValue (suffix);
                }
            }

            return null;
        }

        /// <summary>
        /// Create a derived type like PdfTextField or PdfCheckBox if possible.
        /// If the actual cannot be guessed by PDFsharp the function returns an instance
        /// of PdfGenericField.
        /// </summary>
        PdfAcroField CreateAcroField (PdfDictionary dict)
        {
            var ft = dict.Elements.GetName (Keys.FT);
            var flags = (PdfAcroFieldFlags)dict.Elements.GetInteger (Keys.Ff);
            switch (ft)
            {
                case "/Btn":
                    if ((flags & PdfAcroFieldFlags.Pushbutton) != 0)
                    {
                        return new PdfPushButtonField (dict);
                    }

                    if ((flags & PdfAcroFieldFlags.Radio) != 0)
                    {
                        return new PdfRadioButtonField (dict);
                    }

                    return new PdfCheckBoxField (dict);

                case "/Tx":
                    return new PdfTextField (dict);

                case "/Ch":
                    if ((flags & PdfAcroFieldFlags.Combo) != 0)
                    {
                        return new PdfComboBoxField (dict);
                    }
                    else
                    {
                        return new PdfListBoxField (dict);
                    }

                case "/Sig":
                    return new PdfSignatureField (dict);

                default:
                    return new PdfGenericField (dict);
            }
        }
    }

    /// <summary>
    /// Predefined keys of this dictionary.
    /// The description comes from PDF 1.4 Reference.
    /// </summary>
    public class Keys
        : KeysBase
    {
        // ReSharper disable InconsistentNaming

        /// <summary>
        /// (Required for terminal fields; inheritable) The type of field that this dictionary
        /// describes:
        ///   Btn           Button
        ///   Tx            Text
        ///   Ch            Choice
        ///   Sig (PDF 1.3) Signature
        /// Note: This entry may be present in a nonterminal field (one whose descendants
        /// are themselves fields) in order to provide an inheritable FT value. However, a
        /// nonterminal field does not logically have a type of its own; it is merely a container
        /// for inheritable attributes that are intended for descendant terminal fields of
        /// any type.
        /// </summary>
        [KeyInfo (KeyType.Name | KeyType.Required)]
        public const string FT = "/FT";

        /// <summary>
        /// (Required if this field is the child of another in the field hierarchy; absent otherwise)
        /// The field that is the immediate parent of this one (the field, if any, whose Kids array
        /// includes this field). A field can have at most one parent; that is, it can be included
        /// in the Kids array of at most one other field.
        /// </summary>
        [KeyInfo (KeyType.Dictionary)] public const string Parent = "/Parent";

        /// <summary>
        /// (Optional) An array of indirect references to the immediate children of this field.
        /// </summary>
        [KeyInfo (KeyType.Array | KeyType.Optional, typeof (PdfAcroFieldCollection))]
        public const string Kids = "/Kids";

        /// <summary>
        /// (Optional) The partial field name.
        /// </summary>
        [KeyInfo (KeyType.TextString | KeyType.Optional)]
        public const string T = "/T";

        /// <summary>
        /// (Optional; PDF 1.3) An alternate field name, to be used in place of the actual
        /// field name wherever the field must be identified in the user interface (such as
        /// in error or status messages referring to the field). This text is also useful
        /// when extracting the document�s contents in support of accessibility to disabled
        /// users or for other purposes.
        /// </summary>
        [KeyInfo (KeyType.TextString | KeyType.Optional)]
        public const string TU = "/TU";

        /// <summary>
        /// (Optional; PDF 1.3) The mapping name to be used when exporting interactive form field
        /// data from the document.
        /// </summary>
        [KeyInfo (KeyType.TextString | KeyType.Optional)]
        public const string TM = "/TM";

        /// <summary>
        /// (Optional; inheritable) A set of flags specifying various characteristics of the field.
        /// Default value: 0.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Optional)]
        public const string Ff = "/Ff";

        /// <summary>
        /// (Optional; inheritable) The field�s value, whose format varies depending on
        /// the field type; see the descriptions of individual field types for further information.
        /// </summary>
        [KeyInfo (KeyType.Various | KeyType.Optional)]
        public const string V = "/V";

        /// <summary>
        /// (Optional; inheritable) The default value to which the field reverts when a
        /// reset-form action is executed. The format of this value is the same as that of V.
        /// </summary>
        [KeyInfo (KeyType.Various | KeyType.Optional)]
        public const string DV = "/DV";

        /// <summary>
        /// (Optional; PDF 1.2) An additional-actions dictionary defining the field�s behavior
        /// in response to various trigger events. This entry has exactly the same meaning as
        /// the AA entry in an annotation dictionary.
        /// </summary>
        [KeyInfo (KeyType.Dictionary | KeyType.Optional)]
        public const string AA = "/AA";

        // ----- Additional entries to all fields containing variable text --------------------------

        /// <summary>
        /// (Required; inheritable) A resource dictionary containing default resources
        /// (such as fonts, patterns, or color spaces) to be used by the appearance stream.
        /// At a minimum, this dictionary must contain a Font entry specifying the resource
        /// name and font dictionary of the default font for displaying the field�s text.
        /// </summary>
        [KeyInfo (KeyType.Dictionary | KeyType.Required)]
        public const string DR = "/DR";

        /// <summary>
        /// (Required; inheritable) The default appearance string, containing a sequence of
        /// valid page-content graphics or text state operators defining such properties as
        /// the field�s text size and color.
        /// </summary>
        [KeyInfo (KeyType.String | KeyType.Required)]
        public const string DA = "/DA";

        /// <summary>
        /// (Optional; inheritable) A code specifying the form of quadding (justification)
        /// to be used in displaying the text:
        ///   0 Left-justified
        ///   1 Centered
        ///   2 Right-justified
        /// Default value: 0 (left-justified).
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Optional)]
        public const string Q = "/Q";

        // ReSharper restore InconsistentNaming
    }
}
